Big Data — oder Neue digitale Daten, wie es in der deutschen amtlichen Statistik heißt — sind nicht erst seit heute in vieler Munde. Große Datenmengen in diesem Sinne bedeuten eine immense Hochskalierung von Speichern, Methoden und Werkzeugen gegenüber “traditionellen” Datenmengen. Nun beschäftige ich mich hier nicht mit neueren Big Data-Frameworks oder Cloud Computing, sondern eher mit kleinskaligen, “normalen” R-Geschichten. Mich interessiert also, wo so ungefähr die Grenze verläuft — von Big Data für “normale” Nutzer, die sich mit R und Statistik beschäftigen.
Im normalen Desktop-Gebrauch von R mit RStudio, d. h. ohne
Client-Server-Architektur oder Cloud Computing, werden alle Prozesse im
Hautspeicher (RAM) erledigt. Alle Importdaten, R-Objekte, sowie die für
die Ergebnisse benötigten Rechenkapazitäten nehmen als
R Environments diesen Hauptspeicher ein. Doch dieser ist
auch bei modernen Rechnern begrenzt.
Ich arbeite mit einem recht modernen, trotzdem handelsüblichen Laptop:
Eins vorweg: Wie sich schon aus dem Gesagten ergibt, ist der RAM das ausschlaggebende Nadelöhr. Die vorgestellten Ergebnisse lassen sich wahrscheinlich allein mit diesem Einflussfaktor linear hochrechnen.
Zunächst benötigen wir eine “Big Open Data”-Quelle. Im deutschsprachigem Raum gar nicht so einfach. Ohne langes Suchen nutze ich eine Quelle der American Statistical Association (ASA), die große offene Daten zu Airline On-Time Statistics and Delay Causes bereitstellt. In der Kurzbeschreibung heißt es:
The data consists of flight arrival and departure details for all commercial flights within the USA, from October 1987 to April 2008. This is a large dataset: there are nearly 120 million records in total, and takes up 1.6 gigabytes of space compressed and 12 gigabytes when uncompressed.
Es handelt sich also um Daten zu allen (!) kommerziellen US-amerikanischen Inlandflügen in gut 20 Jahren. Aufgrund dieser Masse beschränken wir uns auf einen Ausschnitt von 2003 bis 2008, also einen Zeitraum von sechs Jahren (…ja, ich habe es mit allen versucht…).
Die Daten sind bz2-komprimiert nach Jahren verfügbar
(Kompressionsfaktor: ~6) und werden mit dem folgenden R-Code beschafft.
Das Gesamtvolumen beträgt ca. 0,67 GByte (komprimiert) bzw. ca. 4,00
GByte (Rohdaten). Um unnötige Downloads zu vermeiden, wird grundsätzlich
mit file.exists geprüft, ob benötigte Eingangsdateien in
dem zugewiesenen Unterordner daten bereits vorhanden sind.
Fehlende Dateien werden anschließend heruntergeladen.
for(i in 2003:2008){
if(!file.exists(paste("daten/",i,".csv.bz2",sep=""))){
url <- paste("http://stat-computing.org/dataexpo/2009/",i,".csv.bz2", sep = "")
download.file(url, destfile = paste("daten/",i,".csv.bz2",sep=""))
}
}Die Dateigröße gibt uns eine erste Einordnung, wo die Grenze der Nutzbarkeit von Big Data auf normalen Laptop/PC-Systemen grob liegt: im GByte-Bereich. Wer “richtig” Big Data macht, bewegt sich eher im Terabyte-Bereich (1 TByte = 1.024 GByte) oder gar im Petabyte-Bereich (1 PByte = 1.024 TByte = 1.048.576 GigaByte).
AlsDatendichte bezeichne ich den theoretischen Wert aus
dem Verhältnis von Information und Overhead. Die
Information ist der konkrete Datenwert, der Overhead ist alles andere,
was Platz belegt (Metadaten, Trennzeichen). Hier ein rudimentäres
Beispiel:
CSV-Datei
ID;Jahr;Alter;Religion;Anzahl
1;2018;35;2;500
Die reine Information ist 2018, 35,
2 und 500. Alle anderen Zeichen sind in diesem
Sinne der Overhead.
Betrachten wir mit diesem Beispiel einige andere typische Datenformate und deren Overhead:
YAML-Datei (“YAML Ain’t Markup Language”)
---
ID: 1
Jahr: 2018
Alter: 35
Religion: 2
Anzahl: 500
JSON-Datei (“JavaScript Object Notation”)
{
"ID": 1,
"Jahr": "2018",
"Alter": 35,
"Religion": 2,
"Anzahl": 500
}
XML-Datei (“Extensible Markup Language”)
<ID>1</ID>
<Jahr>2018</Jahr>
<Alter>35</Alter>
<Religion>2</Religion>
<Anzahl>500</Anzahl>
Die Liste ist nach zunehmender Zeichenanzahl bei gleichem Informationsinhalt, also nach abnehmender Datendichte sortiert. Es ist daraus zu schließen, dass auf Zeichenebene im Beispiel das CSV-Format im Vergleich zum XML-Format nur ein Siebtel des Platzes bei gleichem Informationsinhalt benötigt (die Kopfzeile ist nur einmal deklariert und daher vernachlässigbar).
Natürlich greift dieser Vergleich sehr kurz; die Vorteile und Anwendungsmöglichkeiten der jeweiligen Datenformate ist hier nicht das Thema. Für meine Zwecke zählt an dieser Stelle nur, dass die verwendeten Eingangsdaten in CSV bei begrenzten Ressourcen ein Maximum an Datendichte bzw. an Informationsmenge beinhalten.
Ich vergleiche drei Methoden miteinander:
read.csv,read_csv aus der
Tidyverse-Paketsammlung mit der optimierten
map_df-Funktion des sogenannten functional programming
toolkits purrr, undbind_rows-Funktion (die auch von map_df
genutzt wird)Alle drei Methoden können komprimmierte Rohdaten (z. B. “.zip” oder “.bz2”) direkt entpacken und einlesen. Leider ist das kein unerheblicher Aufwand, der eine Menge Laufzeit frisst. Es läge also nahe, die Dekompression vorzulagern, was wiederum hinsichtlich Reproduzierbarkeit nachteilig ist. Ich vergleiche darum alle Methoden mit und ohne Dekompression, zeige aber nur den kompakteren und reproduzierbaren Code mit der komprimierten Variante der Rohdaten.
Die nach test_import importierten Daten müssen nach
jedem Vorgang mit rm() gelöscht werden, um den RAM frei zu
machen. Alle Variablen werden auf den Datentyp character
fixiert. Das erfordert den minimalen Importaufwand, weil die Daten nicht
umformatiert werden müssen. Ohne diesen Schritt würden alle
Methoden aufgrund der Datenmenge beim “Rowbinding” mit Fehlern
abbrechen.
Die Laufzeit wird wie üblich mit der Funktion
system.time unter Einsatz eines einfachen Zählers
x gemessen. Der gekapselte Code dazwischen ist sozusagen
der Prozess, dessen Laufzeit gemessen wird.
system.time({
test_import <- list.files(path = "daten", full.names = TRUE) %>%
lapply(read.csv, colClasses = "character") %>%
bind_rows
x <- 1:100000
for (i in seq_along(x)) x[i] <- x[i]+1
})
rm(test_import)import_func <- function(dat) {
read_csv(dat, col_types = cols(.default = col_character()))
}
system.time({
test_import <- list.files("daten", full.names = TRUE) %>%
map_df(~import_func(.))
x <- 1:100000
for (i in seq_along(x)) x[i] <- x[i]+1
})
rm(test_import)system.time({
test_import <- list.files(path = "daten", full.names = TRUE) %>%
lapply(read_csv, col_types = cols(.default = col_character())) %>%
bind_rows
x <- 1:100000
for (i in seq_along(x)) x[i] <- x[i]+1
})
dim(test_import)Es werden in jedem Importprozess 42.363.271 Datenzeilen mit je 29 Variablen, also insgesamt 1.228.534.859 (~ 1,2 Mrd.) Werte eingelesen. Die Speicherauslastung kommt dabei auf jeweils über 95 % bei 16 GByte RAM. Die Spitze wird nach dem Einlesen im Prozess des “Rowbindings” erreicht. In dem genutzten technischen Setting bilden die sechs genutzten Dateien die physikalische Grenze der verarbeitbaren Datenmenge. Die Ergebnisse der Laufzeitmessung sind wie folgt:
| Methode | komprimiert (ja/nein) | Laufzeit (sek) |
|---|---|---|
| read.csv | ja | 1011 |
| read_csv mit map_df | ja | 511 |
| read_csv mit bind_rows | ja | 491 |
| read.csv | nein | 383 |
| read_csv mit map_df | nein | 211 |
| read_csv mit bind_rows | nein | 205 |
Anschließend möchte ich testen, wie R mit diesem
Riesen-Tibble umgehen kann. Ich stelle eine einfache Auswertung
in der folgenden Tabelle dar. Das Tibble wird hierfür nach
Year gruppiert und anschließend nach zwei Merkmalen
ausgewertet: die Anzahl der eingesetzten Flugzeuge nach eindeutiger Tail
Number n_distinct(TailNum) sowie die jährliche Gesamtzahl
der Flüge n().
knitr::kable(
test_import %>%
group_by(Year) %>%
summarize(n_distinct(TailNum), n()),
format.args = list(decimal.mark = ",", big.mark = "."),
caption = "Zusammenfassung der Anzahl der eingesetzten Flugzeuge nach eindeutiger Tail Number sowie jährliche Gesamtzahl der Flüge")Mit einem cleveren Management von Im-/Export und den geeigneten Methoden/R-Paketen können auch mit R-Desktop Big Data-Probleme in einer begrenzten Größenordnung angegangen werden. Je größer die Datenmenge, desto wichtiger ist das genutzte Datenformat hinsichtlich Dateigröße und Parsing-Aufwand. Auf einen Formatvergleich in der Verarbeitung habe ich hier verzichtet. Vielleicht teste ich das später einmal aus.
Die optimierte Methode read_csv ist gegenüber der
Basismethode read.csv weitaus effizienter. Der CSV-Import
läuft in fast der doppelten Geschwindigkeit ab. Kommt der
Dekomprimierungsprozess beim Import hinzu, läuft die optimierte Variante
sogar mehr als doppelt so schnell. Der “funktionalere Ansatz” mit
map_df ist gegenüber lapply /
bind_rows leicht unterlegen, aber vergleichbar
effizient.
Sind die Daten erst einmal als Tibble eingelesen, laufen einfache
Analysefunktionen (z. B. n() oder sum()) und
Gruppierungen mit group_by() erstaunlich effizient ab. Es
spielt hier keine spürbare Rolle, wie groß der betrachtete Datensatz
ist. Mit der Weiterverarbeitung von “Big Data mit R Desktop” will ich
mich später bei Gelegenheit vertiefend beschäftigen…
## R version 4.3.3 (2024-02-29)
## Platform: x86_64-pc-linux-gnu (64-bit)
## Running under: Manjaro Linux
##
## Matrix products: default
## BLAS: /usr/lib/libblas.so.3.12.0
## LAPACK: /usr/lib/liblapack.so.3.12.0
##
## locale:
## [1] LC_CTYPE=de_DE.UTF-8 LC_NUMERIC=C LC_TIME=de_DE.UTF-8 LC_COLLATE=de_DE.UTF-8 LC_MONETARY=de_DE.UTF-8
## [6] LC_MESSAGES=de_DE.UTF-8 LC_PAPER=de_DE.UTF-8 LC_NAME=C LC_ADDRESS=C LC_TELEPHONE=C
## [11] LC_MEASUREMENT=de_DE.UTF-8 LC_IDENTIFICATION=C
##
## time zone: Europe/Berlin
## tzcode source: system (glibc)
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] highcharter_0.9.4 kableExtra_1.4.0 DT_0.32 stopwords_2.3 tidytext_0.4.1 RColorBrewer_1.1-3 ggthemes_5.1.0
## [8] lubridate_1.9.3 forcats_1.0.0 stringr_1.5.1 dplyr_1.1.4 purrr_1.0.2 readr_2.1.5 tidyr_1.3.1
## [15] tibble_3.2.1 ggplot2_3.5.0 tidyverse_2.0.0
##
## loaded via a namespace (and not attached):
## [1] tidyselect_1.2.1 viridisLite_0.4.2 farver_2.1.1 fastmap_1.1.1 janeaustenr_1.0.0 digest_0.6.35 timechange_0.3.0 lifecycle_1.0.4
## [9] tokenizers_0.3.0 magrittr_2.0.3 compiler_4.3.3 rlang_1.1.3 sass_0.4.9 tools_4.3.3 igraph_2.0.3 utf8_1.2.4
## [17] yaml_2.3.8 data.table_1.15.4 knitr_1.45 labeling_0.4.3 htmlwidgets_1.6.4 bit_4.0.5 curl_5.2.1 xml2_1.3.6
## [25] TTR_0.24.4 withr_3.0.0 grid_4.3.3 fansi_1.0.6 xts_0.13.2 colorspace_2.1-0 scales_1.3.0 cli_3.6.2
## [33] rmarkdown_2.26 crayon_1.5.2 generics_0.1.3 rlist_0.4.6.2 rstudioapi_0.16.0 tzdb_0.4.0 cachem_1.0.8 assertthat_0.2.1
## [41] parallel_4.3.3 vctrs_0.6.5 Matrix_1.6-5 jsonlite_1.8.8 hms_1.1.3 bit64_4.0.5 crosstalk_1.2.1 systemfonts_1.0.6
## [49] fontawesome_0.5.2 jquerylib_0.1.4 quantmod_0.4.26 glue_1.7.0 stringi_1.8.3 gtable_0.3.4 munsell_0.5.0 pillar_1.9.0
## [57] htmltools_0.5.8 R6_2.5.1 vroom_1.6.5 evaluate_0.23 lattice_0.22-5 highr_0.10 backports_1.4.1 SnowballC_0.7.1
## [65] broom_1.0.5 bslib_0.7.0 Rcpp_1.0.12 svglite_2.1.3 xfun_0.43 zoo_1.8-12 pkgconfig_2.0.3
Dieses Werk ist lizenziert unter einer Creative Commons Attribution-ShareAlike 4.0 International License.